<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the following classes:
 *
 * - View
 * - View_Exception
 * - View_NotFoundException
 *
 * PHP version 5
 *
 * This file is part of Majic - A rapid-development web application framework
 * for PHP 5.
 *
 * Majic is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Majic is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Majic. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     View
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007-2008 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: View.php 36 2008-08-09 23:31:49Z mjijackson $
 */

/**
 * Loads views and layouts. Also takes care of outputting application output
 * to the browser, and optionally caching output that has already been sent.
 *
 * @package     View
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class View
{

    /**
     * The output buffer level. Useful for keeping track of the output buffer
     * level when processing nested layouts.
     *
     * @var     int
     * @access  private
     */
    private $ob_level;

    /**
     * The output string.
     *
     * @var     string
     * @access  private
     */
    private $output;

    /**
     * An array of headers to send before sending the output to the client. May
     * be set using {@link addHeader() addHeader}.
     *
     * @var     array
     * @access  private
     */
    private $headers = array();

    /**
     * An array of variables that will be passed to every view. May be set using
     * {@link setVars() setVars}.
     *
     * @var     array
     * @access  private
     */
    private $vars = array();

    /**
     * Constructor. Sets this viewer's output buffer level.
     *
     * @access  private
     */
    private function __construct()
    {
        $this->ob_level = ob_get_level();
    }

    /**
     * Gets the instance of the View class.
     *
     * @return  View      The View singleton object
     * @access  public
     * @static
     */
    public static function getInstance()
    {
        static $inst;
        if (!$inst) {
            $inst = new self();
        }
        return $inst;
    }

    /**
     * Sets some variables that will be passed to every view.
     *
     * @param   array   $vars       An array of variables
     * @param   mixed   $value      The value to use
     * @return  void
     * @access  public
     */
    public function setVars($vars, $value = null)
    {
        if (!is_array($vars)) {
            $vars = array($vars => $value);
        }

        $this->vars = array_merge($this->vars, $vars);
    }

    /**
     * Gets the output string.
     *
     * @return  string      The output string
     * @access  public
     */
    public function getOutput()
    {
        return $this->output;
    }

    /**
     * Sets the output string.
     *
     * @param   string      $output     The output string
     * @return  void
     * @access  public
     */
    public function setOutput($output)
    {
        $this->output = $output;
    }

    /**
     * Adds a header to the output.
     *
     * @param   string      $header     The header to add to the output
     * @return  void
     * @access  public
     */
    public function addHeader($header)
    {
        $this->headers[] = $header;
    }

    /**
     * Sends the output to the browser.
     *
     * @return  void
     * @access  public
     */
    public function send()
    {
        foreach ($this->headers as $header) {
            header($header);
        }
        echo $this->output;
    }

    /**
     * Loads a view with the given variables.
     *
     * @param   string  $view       The name of the view
     * @param   array   $vars       Some variables to pass to the view
     * @param   bool    $return     True to return a string of the output
     * @return  mixed               String if $return is true, nothing otherwise
     * @throws  View_NotFoundException      If the corresponding view file
     *                                      cannot be found
     * @access  public
     */
    public function load($view, $vars = array(), $return = false)
    {
        if (!is_array($vars)) {
            $vars = array();
        }

        if (file_exists(APP_DIR . "/views/$view.php")) {
                // extract vars into local symbol table
                extract(array_merge($this->vars, $vars));
                // start output buffering
                ob_start();

                include APP_DIR . "/views/$view.php";
        } else {
            throw new View_NotFoundException($view);
        }

        if ($return) {
            $buffer = ob_get_contents();
            @ob_end_clean();
            return $buffer;
        }

        if (ob_get_level() > $this->ob_level + 1) {
            ob_end_flush();
        } else {
            $this->setOutput(ob_get_contents());
            @ob_end_clean();
        }
    }

    /**
     * Loads a view inside a layout.
     *
     * @param   string  $view       The name of the view
     * @param   mixed   $layouts    The name of the layout or an array of nested
     *                              layouts
     * @param   array   $vars       Some variables to pass to the view
     * @param   bool    $return     True to return a string of the output
     * @return  mixed               String if $return is true, void otherwise
     * @access  public
     */
    public function layout($view, $layouts = 'default', $vars = array(), $return = false)
    {
        if (!is_array($vars)) {
            $vars = array();
        }
        if (!is_array($layouts)) {
            $layouts = array($layouts);
        }

        $contents = $this->load($view, $vars, true);
        foreach ($layouts as $layout) { // allow nested layouts
            $contents = $this->load("layouts/$layout", array_merge($vars, array(
                'layout_content' => $contents
            )), true);
        }

        if ($return) {
            return $contents;
        } else {
            $this->setOutput($contents);
        }
    }

}

/**
 * The base class for all view exceptions.
 *
 * @package     View
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class View_Exception extends Majic_Exception {}

/**
 * View not found exception. Thrown when a view file cannot be found.
 *
 * @package     View
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class View_NotFoundException extends Majic_Exception
{

    /**
     * Constructor.
     *
     * @param   string  $view           The name of the view that couldn't be
     *                                  found
     * @access  public
     */
    public function __construct($view)
    {
        parent::__construct("View '$view' could not be found");
    }

}

?>
